package masterjava.common.client;

import masterjava.common.exception.ClientUnavailableException;
import masterjava.common.exception.FaultFactory;
import masterjava.common.logging.MasterLogger;
import masterjava.common.property.IPropertyListener;
import masterjava.common.property.Property;
import masterjava.common.state.State;
import masterjava.common.ws.WebServiceFault;

import java.lang.reflect.*;

/**
 * MasterJava.ru
 * <p/>
 * User: GKislin
 * Date: 01.03.11
 */

public abstract class AbstractClient<ITF, PROP> {
    protected static MasterLogger LOGGER = MasterLogger.get(AbstractClient.class);

    protected final String serviceName;
    protected ITF wrappedClient;
    protected State state = new State();
    protected ClientInvocationHandler handler;
    protected final Class<ITF> serviceClass;
    private volatile boolean initialized = false;
    protected final Property<PROP> serviceLocation;

    @SuppressWarnings("unchecked")
    public AbstractClient(String name, Property<PROP> serviceLocation) {
        this.serviceName = name;
        this.serviceLocation = serviceLocation;
        serviceLocation.addListener(new IPropertyListener<PROP>() {
            @Override
            public void update(Property<PROP> prop, PROP oldValue) {
                reInitUnsafe();
            }
        });
        this.handler = new ClientInvocationHandler();
        serviceClass = (Class<ITF>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        this.wrappedClient = (ITF) Proxy.newProxyInstance(
                serviceClass.getClassLoader(),
                new Class[]{serviceClass}, handler);
    }

    public boolean isInitialized() {
        return initialized;
    }

    protected abstract boolean isClientUnavailable(Throwable e);

    protected abstract boolean isClientReinitialize(Throwable e);

    protected abstract ITF doInit() throws Exception;

    public ITF get0() throws ClientUnavailableException {
        init();
        return wrappedClient;
    }

    public ITF get() throws WebServiceFault {
        try {
            return get0();
        } catch (ClientUnavailableException e) {
            throw FaultFactory.createFault(e);
        }
    }

    public void init() throws ClientUnavailableException{
        if(!initialized){
            handler.init();
        }
    }

    public void reInitUnsafe() {
        try {
            reInit();
        } catch (ClientUnavailableException e) {
            // already logged, no action
        }
    }

    public void reInit() throws ClientUnavailableException {
        initialized = false;
        handler.init();
    }

    private class ClientInvocationHandler implements InvocationHandler {
        private ITF client = null;

        public void init() throws ClientUnavailableException {
            // init only once
            synchronized (this) {
                if (!initialized) {
                    LOGGER.warn("Init " + serviceName);
                    try {
                        client = doInit();
                        state.set("%s initialized", serviceName);
                        initialized = true;
                    } catch (Throwable t) {
                        checkUnavailable(t);
                        state.setError("%s client initialization exception", t, serviceName);
                        throw LOGGER.getIllegalStateException(state.getState(), t);
                    }
                }
            }
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                LOGGER.info("Call " + serviceName + "." + method.getName());
                return method.invoke(client, args);
            } catch (InvocationTargetException e) {
                Throwable target = e.getTargetException();
                if (isClientReinitialize(target)) {
                    LOGGER.warn("ReInit " + serviceName, target);
                    reInit();
                    try {
                        return method.invoke(client, args);
                    } catch (InvocationTargetException ex) {
                        throw ex.getTargetException();
                    }
                } else {
                    checkUnavailable(target);
                    throw target;
                }
            }
        }

        private void checkUnavailable(Throwable t) throws ClientUnavailableException{
            if (isClientUnavailable(t)) {
                state.setWarning("%s client unavailable (%s)", null, serviceName, t.toString());
                throw new ClientUnavailableException(serviceName, state.getState());
            }
        }
    }
}
